# forward-compat boilerplate
from __future__ import absolute_import
from __future__ import with_statement
__metaclass__ = type

import sqlalchemy.orm as orm

from fenton import util
from fenton import view
from fenton import types
from fenton import logging


def compile_all():
    for m in AbstractMeta.all_metas:
        m.compile()
    for m in AbstractMeta.all_metas:
        m.register()
    orm.compile_mappers()


def create_class(name=None, bases=None, metaclass=None, **kw):
    if name is None:
        name = '<dynamic base class>'
    if bases is None:
        bases = (object,)
    if metaclass is None:
        metaclass = type
    kw['__metaclass__'] = metaclass
    Class = metaclass(name, bases, kw)
    Class.__module__ = '<dynamic>'
    return Class


def create_base_class(metainit,
                      metaclass=type,
                      classfactory=create_class,
                      **kw):
    base_class = None
    class submeta(metaclass):
        supermeta = metaclass
        def __init__(self, name, bases, attrs):
            metaclass.__init__(self, name, bases, attrs)
            if base_class and self is not base_class:
                metainit(self)
    kw['metaclass'] = submeta
    base_class = classfactory(**kw)
    return base_class


def _idlist(objs):
    out = []
    for obj in objs:
        meta = getmeta(obj, False)
        if meta:
            out.extend(_idlist(getattr(obj, k) for k in meta.key))
        else:
            out.append(obj)
    return out




class IncorrectArgs(types.Invalid):
    pass




class NoInstance(types.Invalid):
    def get_message(self):
        return 'No instance: %s' % (self.input)




class Registry:

    class KeyExists(Exception):
        'Attempt to register an existing key'

    def __init__(self, sep='/'):
        self.__items = {}
        self.__sep = sep

    def __contains__(self, path):
        return path in self.__items

    def set(self, path, obj, force=False):
        path = self.join(self.split(path))
        obj.path = path
        if path in self.__items:
            if force:
                logging.log.warn("Re-registration: '%s'" % path)
            else:
                raise self.KeyExists(path)
        self.__items[path] = obj
        return obj

    def get(self, path):
        if not path:
            path = self.__sep
        parts = self.split(path)
        # reversed() makes the matching greedy
        # i.e. match /foo/bar before /foo
        for i in reversed(range(len(parts))):
            key = self.join(parts[0:i+1])
            if key in self.__items:
                rest = parts[i+1:]
                return self.__items[key], self.join(rest)
        raise KeyError(path)

    def split(self, path):
        parts = path.split(self.__sep)
        if not parts[0]:
            parts.pop(0)
        return parts

    def join(self, parts):
        return self.__sep.join(parts)

    def register(self, meta, *path):
        self.set(self.join(path), meta)

    def __repr__(self):
        s = ', '.join('%r' % x for x in sorted(self.__items.keys()))
        return '<Registry (%s)>' % s




class AbstractMeta:

    model_class = None
    fields = None
    privs = None
    all_metas = []
    meta_map = {}
    class_map = {}
    __context_attr = '____fenton_model_context____'
    __meta_attr = '____fenton_model_meta____'


    def __init__(self, model_class):
        setattr(model_class, self.__meta_attr, self)
        self.__children = []
        self.__parents = ()
        self.model_class = model_class
        self.all_metas.append(self)
        self.class_map[model_class.__name__] = self
        if self.classid:
            self.meta_map[self.classid] = self


    def __repr__(self):
        return '<%s: %s>' % (self.__class__.__name__,
                            util.classname(self.model_class))

    @classmethod
    def getmeta(Class, obj, throw=True):
        if isinstance(obj, AbstractMeta):
            return obj
        try:
            return getattr(obj, Class.__meta_attr)
        except AttributeError:
            if throw:
                raise
            return None


    # decorator
    @classmethod
    def declare(Class, **kw):
        def classdecorator(newclass):
            return Class.create_base(bases=(newclass,), **kw)
        return classdecorator


    @classmethod
    def create_base(Class, **kw):
        if 'bases' not in kw:
            kw['bases'] = (Class.base_class,)
        factory = kw.pop('classfactory', Class.base_class_factory)
        metaclass = kw.pop('metaclass', None) or Class.metaclass
        return create_base_class(Class,
                                 classfactory=factory,
                                 metaclass=metaclass,
                                 **kw)

    def compile(self):
        mc = self.model_class
        ### compile permissions
        self.privs = getattr(mc, '__permissions__', {})

        ### compile model key
        key = getattr(mc, '__key__', None) or ()
        if not isinstance(key, (tuple, list)):
            key = (key,)
        self.key = key

        ### compile model attributes
        attrs = self.attributes = {}
        for c in reversed(mc.__mro__):
            attrs.update(getattr(c, '__attrs__', {}))
        for k, t in attrs.iteritems():
            t.set_owner(self, k)

        ischild = False

        ### compile hierarchy
        parents = mc.__dict__.get('__parent__', None)
        if isinstance(parents, type(lambda:0)):
            parents = parents()
        if not isinstance(parents, tuple):
            parents = parents and (parents,) or ()
        self.__parents = parents
        for parent in parents:
            if isinstance(parent, basestring):
                parent = self.attributes.get(parent)
                parent = parent and parent.meta
                #ischild = True
            elif isinstance(parent, type):
                parent = getmeta(parent)
            if parent and isinstance(parent, AbstractMeta):
                parent.add_child(self)

        ### compile form
        self.fields = getattr(mc, '__fields__', None) or ()
        for a, b, fs in self.fields:
            for f in fs:
                if f not in self.attributes:
                    raise TypeError('Missing attr %s in %s' % (f, mc))

        # compile screens
        view_screen = view.ViewScreen
        if ischild:
            edit_screen = view.SubEditScreen
            create_screen = view.SubCreateScreen
        else:
            edit_screen = view.EditScreen
            create_screen = view.CreateScreen

        self.view_screen = getattr(mc, '__view__', view_screen)
        self.edit_screen = getattr(mc, '__edit__', edit_screen)
        self.create_screen = getattr(mc, '__create__', create_screen)


    def register(self):
        path = self.get_class_path()
        if path and path not in REGISTRY:
            REGISTRY.register(self, path)


    def add_child(self, child):
        if child not in self.__children:
            self.__children.append(child)


    def get_context(self, obj):
        return getattr(obj, self.__context_attr)


    def set_context(self, context, obj):
        setattr(obj, self.__context_attr, context)


    def get_id(self, context, obj):
        return self.simplify(obj, context)


    def get_class_path(self):
        if not self.classid:
            return None
        if self.get_polymorphic_attr():
            meta = filter(None, [getmeta(C, False) for C in self.model_class.__mro__])[-1]
        else:
            meta = self
        path = meta.classid.replace('_', '-').split('.')
        return REGISTRY.join(path)


    def get_object_path(self, context, obj):
        path = self.get_class_path()
        if not path:
            return None
        id = self.get_id(context, obj)
        if id:
            return REGISTRY.join((path, id))
        return path


    @util.lazyattr
    def key_validators(self):
        return tuple(self.attributes[k].get_validator() for k in self.key)


    @util.lazyattr
    def num_args(self):
        return sum(v.num_args for v in self.key_validators)


    def get_view(self, context, obj, parent=None):
        priv = self.get_priv('view')
        return self.view_screen(context, obj, priv, parent)


    def get_checked_view(self, context, obj, parent=None):
        v = self.get_view(context, obj, parent)
        if v.check():
            return v


    def __get__view__(self, context, obj, parent=None):
        priv = self.get_priv('view')
        vs = self.view_screen
        from fenton import security
        if vs and security.check(priv, context):
            v = vs(context, obj, priv, parent)
            if v.get_path():
                return v


    def get_parents(self, context, obj):
        result = []
        for p in self.__parents:
            if isinstance(p, basestring):
                p = getattr(obj, p, None)
            if p:
                if isinstance(p, type):
                    p = getmeta(p).load_model(context, None)
                result.append(p)
        return tuple(result)


    def get_children(self, context):
        # XXX args?
        args = ()
        for meta in self.__children:
            yield meta.load_model(context, args)


    def get_priv(self, key):
        return self.privs and self.privs.get(key) or None


    def send_event(self, __obj, __event, *args, **kw):
        f = getattr(__obj, __event + '_event', None)
        if f:
            return f(*args, **kw)
        return False


    def get_polymorphic_attr(self):
        pass


    def get_polymorphic_id(self):
        pass


    def get_polymorphic_meta(self, polyattr, polyid):
        raise NotImplementedError


    def construct_child(self, context, obj, key, index=None):
        subtype = self.attributes[key]
        submeta = subtype.meta

        # the child must deal with attaching the parent correctly
        pattr = submeta.__parents[0]
        attrs = {pattr: obj}
        # should be:
        # attrs = {'__parent__': obj}

        attr = submeta.get_polymorphic_attr()
        if attr:
            attrs[attr] = submeta.get_polymorphic_id()
        if index is not None and subtype.index_attr:
            attrs[subtype.index_attr] = index
        # attrs[subtype.key_attr] = key
        return submeta.construct_model(context, **attrs)


    def delete_child(self, context, obj, key, index):
        if index is not None:
            subtype = self.attributes[key]
            items = getattr(obj, key)
            del items[index]
            # reindex children
            if subtype.index_attr:
                for i in range(index, len(items)):
                    setattr(items[index], subtype.index_attr, i)
        else:
            delattr(obj, key)


    def store_child(self, context, obj, child, key, index):
        subtype = self.attributes[key]
        if index is None:
            val = child
        else:
            val = getattr(obj, key)
            if index == len(getattr(obj, key)):
                val.append(child)
        # log history
        self.set_attribute(context, obj, key, val, None)


    def construct_model(self, context, **args):
        polyattr = self.get_polymorphic_attr()
        polyid = polyattr and args.get(polyattr) or None
        if polyid:
            meta = self.get_polymorphic_meta(polyid)
        else:
            meta = self
        obj = meta.do_construct_model(context, args)
        meta.send_event(obj, 'after_construct')
        return obj


    def do_construct_model(self, context, args):
        raise NotImplementedError


    def purge_model(self, context, obj):
        return obj


    def set_attribute(self, context, obj, key, value, form=None):
        if not self.send_event(obj, 'set_attribute', key, value, form):
            setattr(obj, key, value)


    def update_model(self, context, obj, vars, form=None):
        self.send_event(obj, 'before_update', form)
        self.do_update_model(context, obj, vars, form)
        self.send_event(obj, 'after_update', form)


    def do_update_model(self, context, obj, args, form):
        for k in args:
            self.set_attribute(context, obj, k, args[k], form)


    def polyswitch(self, context, obj, args):
        polyattr = self.get_polymorphic_attr()
        polyid = polyattr and args.get(polyattr) or None
        if polyid and getattr(obj, polyattr) != polyid:
            newmeta = self.get_polymorphic_meta(polyid)
            # preserve old attributes ???
            return newmeta.construct_model(context)
        return obj


    def load_model(self, context, args):
        obj = self.do_load_model(context, args)
        self.send_event(obj, 'after_load')
        return obj


    def do_load_model(self, context, args):
        raise NotImplementedError


    def iter_model(self, context, filter):
        raise NotImplementedError


    def store_model(self, context, obj):
        self.thaw_model(context, obj)
        self.send_event(obj, 'before_store')
        obj = self.do_store_model(context, obj)
        self.send_event(obj, 'after_store')
        return obj


    def do_store_model(self, context, model):
        raise NotImplementedError


    def thaw_model(self, context, obj):
        self.send_event(obj, 'before_thaw')
        self.do_thaw_model(context, obj)
        self.send_event(obj, 'after_thaw')
        return obj


    def do_thaw_model(self, context, obj):
        self.set_context(context, obj)
        return obj


    def delete_model(self, context, obj):
        self.send_event(obj, 'before_delete')
        self.do_delete_model(context, obj)
        self.send_event(obj, 'after_delete')


    def do_delete_model(self, context, model):
        raise NotImplementedError


    def format_attribute(self, obj, attr, context):
        v = getattr(obj, attr, None)
        if v is not None:
            return self.attributes[attr].format_value(v, context)
        else:
            return ''


    def reconstruct(self, arg, context):
        if arg:
            args = REGISTRY.split(arg)
        else:
            args = ()
        if len(args) != self.num_args:
            logging.log.info('%r: Expected %d args, received %d' % (self, self.num_args, len(args)))
            raise IncorrectArgs(arg, context)

        py_args = []
        i = 0
        # recursively validate the arguments
        for v in self.key_validators:
            n = getattr(v, 'num_args', 1)
            x = REGISTRY.join(args[i:i+n])
            py_args.append(v.reconstruct(x, context))
            i += n

        if i != self.num_args:
            msg = i < self.num_args and 'few' or 'many'
            logging.log.info('Too %s arguments: expected %d, got %d' % (msg, self.num_args, i))
            raise IncorrectArgs(arg, context)

        obj = self.load_model(context, py_args)
        if obj is None:
            raise NoInstance(input=arg)
        return obj


    def simplify(self, obj, context):
        if obj is None:
            return ''
        bits = (unicode(v.simplify(getattr(obj, k), context))
                for (k, v) in zip(self.key, self.key_validators))
        return REGISTRY.join(bits)


getmeta = AbstractMeta.getmeta




class ModelMeta(AbstractMeta):
    metaclass = type
    base_class = object
    base_class_factory = staticmethod(create_class)

    def do_construct_model(self, context, args):
        obj = self.model_class()
        self.do_thaw_model(context, obj)
        if args:
            self.do_update_model(context, obj, args, form=None)
        return obj

    def do_load_model(self, context, args):
        if args:
            args = dict(zip(self.key, args))
        return self.do_construct_model(context, args)

    @util.lazyattr
    def title(self):
        return util.decamel(self.model_class.__name__).replace('_', ' ').title()

    @util.lazyattr
    def classid(self):
        if '__classid__' in self.model_class.__dict__:
            return self.model_class.__classid__
        prefix = self._classid_prefix()
        name = self._classid_name()
        if prefix:
            name = '%s.%s' % (prefix, name)
        return name

    @property
    def module(self):
        import sys
        return sys.modules[self.model_class.__module__]

    def _classid_prefix(self):
        if hasattr(self.model_class, '__model_prefix__'):
            return self.model_class.__model_prefix__
        if hasattr(self.module, '__fenton_prefix__'):
            return self.module.__fenton_prefix__
        prefix = self.module.__name__.split('.')[1:]
        return '.'.join(map(util.decamel, prefix)).lower()

    def _classid_name(self):
        if hasattr(self.model_class, '__model_name__'):
            return self.model_class.__model_name__
        return util.decamel(self.model_class.__name__)




class ModelIterator:
    filter = None
    link_column = None
    list_columns = []
    __view__ = view.ListScreen

    submeta = property(lambda x:getmeta(x).submeta)

    def __iter__(self):
        context = getmeta(self).get_context(self)
        return iter(self.submeta.iter_model(context, self.filter))

    def __title__(self):
        return getmeta(self).title




class IterMeta(ModelMeta):
    submeta = None
    get_priv = property(lambda x:x.submeta.get_priv)
    base_class = ModelIterator

    def add_child(self, child):
        if not self.submeta:
            self.submeta = child




class DbMeta(ModelMeta):

    @staticmethod
    def base_class_factory(**kw):
        if 'bases' in kw:
            kw['cls'] = kw.pop('bases')
        from sqlalchemy.ext.declarative import declarative_base
        return declarative_base(**kw)

    @util.classproperty
    def metaclass(self):
        from sqlalchemy.ext.declarative import DeclarativeMeta
        return DeclarativeMeta

    def get_db(self, context):
        raise NotImplementedError

    def _query(self, context):
        return self.get_db(context).query(self.model_class)

    def do_load_model(self, context, args):
        if args:
            args = _idlist(args)
        else:
            args = ()
        obj = self._query(context).get(args)
        if obj is not None:
            self.set_context(context, obj)
        return obj

    def iter_model(self, context, filter):
        q = self._query(context)
        if self.model_class._meta:
            eager = orm.contains_eager(self.model_class._meta)
            q = q.join(self.model_class._meta).options(eager)

        q = self.get_default_filter()(q)
        if filter:
            q = filter(q)
        return q

    def get_default_filter(self):
        return getattr(self.model_class, '__filter__', lambda q: q)

    def do_thaw_model(self, context, obj):
        #print 'do_thaw: <%s 0x%x>' % (type(obj).__name__, id(obj))
        db = self.get_db(context)
        # FIXME: add() is wrong
        db.add(obj)
        self.set_context(context, obj)
        return obj

    def do_store_model(self, context, obj):
        db = self.get_db(context)
        db.add(obj)
        return obj

    def do_delete_model(self, context, obj):
        self.get_db(context).delete(obj)

    def purge_model(self, context, obj):
        db = self.get_db(context)
        if obj not in db.new:
            try:
                db.refresh(obj)
            except sql.exc.InvalidRequestError:
                pass
        return obj



REGISTRY = Registry()



